package pkg.andru.animation;

import java.util.HashMap;

import org.xmlpull.v1.XmlPullParser;

import pkg.andru.animation.Frame.FrameDrawState;
import pkg.andru.animation.Sprite.SpriteDrawState;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;

/**
 * 
 * @author idiottiger
 * 
 */
public class AnimeParser {

    static final String TAG = "AnimeParser";

    static final String ERROR_FORMAT_MESSAGE = "line:%d, column:%d, error:%s";

    private Context mContext;
    private Resources mResources;
    private HashMap<String, String> mKeyValueTempMap = new HashMap<String, String>();
    private AnimeResourceCache mResourceCache;
    private String mPkgName;

    final BitmapFactory.Options options = new BitmapFactory.Options();

    public AnimeParser(Context context) {
        mContext = context;
        mPkgName = mContext.getPackageName();
        mResources = mContext.getResources();

        mResourceCache = AnimeResourceCache.getInstance();

        options.inDither = false;
        options.inPreferredConfig = Bitmap.Config.RGB_565;
    }

    /**
     * parse anime
     * 
     * @param animeId
     * @param framesId
     * @param spritesId
     * @return
     * @throws AnimeParseException
     */
    public Anime parseAnime(int animeId, int framesId, int spritesId) throws AnimeParseException {

        Anime anime = parseAnime(animeId);
        // for now we need parse the frames to get the drawstate's contained id
        // frame
        parseFrames(anime, framesId);

        // parse sprites
        parseSprites(anime, spritesId);

        return anime;
    }

    private Anime parseAnime(int animeId) throws AnimeParseException {
        Anime anime = null;
        XmlResourceParser parser = null;
        try {
            parser = mResources.getXml(animeId);
            int eventType = parser.getEventType();

            // start parse anime
            boolean passRoot = false;
            Log.i(TAG, "start parse anim file");
            while (eventType != XmlPullParser.END_DOCUMENT) {
                if (eventType == XmlPullParser.START_TAG) {
                    String tagName = parser.getName();
                    Log.i(TAG, "TAG:" + tagName);
                    // the aime must have 'id' attribute
                    if (tagName.equalsIgnoreCase(DEF.ROOT_ANIME_KEY)) {
                        // check the root
                        if (passRoot) {
                            throwAnimeParseException(parser, "<" + tagName + "> tag must be the root tag");
                        }
                        updateKeyValueMap(parser);
                        if (mKeyValueTempMap.containsKey(DEF.KEY_ID)) {
                            anime = new Anime();
                            try {
                                final int id = Integer.valueOf(mKeyValueTempMap.get(DEF.KEY_ID));
                                anime.setId(id);
                            } catch (NumberFormatException e) {
                                throwAnimeParseException(parser, e.getMessage());
                            }
                            String name = mKeyValueTempMap.get(DEF.KEY_NAME);
                            if (name != null) {
                                anime.setName(name);
                            }
                        } else {
                            throwAnimeParseException(parser, String.format("<%s> tag must has the '%s' attribute", tagName, DEF.KEY_ID));
                        }
                    } else if (tagName.equalsIgnoreCase(DEF.TAG_FRAME)) {
                        // check the root
                        if (!passRoot) {
                            throwAnimeParseException(parser, "<" + tagName + "> can not be the root tag");
                        }
                        // must have 'id' and 'duration'
                        updateKeyValueMap(parser);
                        if (mKeyValueTempMap.containsKey(DEF.KEY_ID) && mKeyValueTempMap.containsKey(DEF.KEY_DURATION)) {
                            try {
                                final int id = Integer.valueOf(mKeyValueTempMap.get(DEF.KEY_ID)); // id
                                final int duration = Integer.valueOf(mKeyValueTempMap.get(DEF.KEY_DURATION)); // duration

                                // init default value
                                float x = 0, y = 0, alpha = 1;
                                boolean keeped = false;

                                String value = null;

                                // here the x and y value need check the dimen
                                // of not
                                if ((value = mKeyValueTempMap.get(DEF.KEY_X)) != null) { // x
                                    x = getValue(parser, value);
                                }
                                if ((value = mKeyValueTempMap.get(DEF.KEY_Y)) != null) { // y
                                    y = getValue(parser, value);
                                }
                                if ((value = mKeyValueTempMap.get(DEF.KEY_KEEPD)) != null) { // keeped
                                    keeped = Boolean.valueOf(value);
                                }
                                if ((value = mKeyValueTempMap.get(DEF.KEY_ALPHA)) != null) { // alpha
                                    alpha = Float.valueOf(value);
                                }

                                // state
                                FrameDrawState state = new FrameDrawState(id, x, y, duration, alpha, keeped);
                                Log.i(TAG, state.toString());

                                // if not existed, put to the resource pool,
                                // need later to
                                // init the frame's all sprite, current just
                                // init the id
                                Frame frame = mResourceCache.getFrame(id);
                                if (frame == null) {
                                    frame = new Frame();
                                    frame.setId(id);
                                    mResourceCache.putFrame(frame);
                                }

                                // add to the anime
                                anime.addFrame(frame, state);
                            } catch (NumberFormatException e) {
                                throwAnimeParseException(parser, e.getMessage());
                            }
                        } else {
                            throwAnimeParseException(parser, "<" + tagName + "> tag must has the 'id' and 'duration' attributes");
                        }

                    }
                    passRoot = true;
                }
                eventType = parser.next();
            }
            Log.i(TAG, "end parse anim file");
        } catch (Exception e) {
            throw new AnimeParseException(e.getMessage());
        }
        if (parser != null) {
            parser.close();
        }

        return anime;

    }

    private void parseFrames(Anime anime, int framesId) throws AnimeParseException {
        XmlResourceParser parser = null;
        try {
            parser = mResources.getXml(framesId);
            int eventType = parser.getEventType();

            Frame frame = null;
            Sprite sprite = null;
            SpriteDrawState spriteDrawState = null;

            // start parse anime
            boolean passRoot = false, skipThisFrame = false;
            Log.i(TAG, "start parse frame file");
            while (eventType != XmlPullParser.END_DOCUMENT) {
                String tagName = parser.getName();
                if (eventType == XmlPullParser.START_TAG) {
                    Log.i(TAG, "TAG:" + tagName);
                    // check the root
                    if (tagName.equalsIgnoreCase(DEF.ROOT_FRAMES_KEY)) {
                        if (passRoot) {
                            throwAnimeParseException(parser, "<" + tagName + "> tag must be the root tag");
                        }
                    } else if (!passRoot) {
                        throwAnimeParseException(parser, "<" + tagName + "> can not be the root tag");
                    } else if (tagName.equalsIgnoreCase(DEF.TAG_FRAME)) {
                        // the frame
                        updateKeyValueMap(parser);
                        if (mKeyValueTempMap.containsKey(DEF.KEY_ID)) {
                            try {
                                final int id = Integer.valueOf(mKeyValueTempMap.get(DEF.KEY_ID));

                                frame = null;
                                // check the anime need use this or not
                                int count = anime.getFrameCount();
                                for (int i = 0; i < count; i++) {
                                    Frame temp = anime.getFrame(i);
                                    if (temp.getId() == id) {
                                        frame = temp;
                                        break;
                                    }
                                }

                                // dont use this frame , can skip this
                                if (frame == null) {
                                    skipThisFrame = true;
                                } else {
                                    // check the resource pool, if pool dont
                                    // have,
                                    // just add it to pool
                                    if (mResourceCache.getFrame(id) == null) {
                                        mResourceCache.putFrame(frame);
                                    }
                                    // if the frame is not blank, mean init
                                    // before, so also skip this frame
                                    if (!frame.isBlankFrame()) {
                                        skipThisFrame = true;
                                    }
                                }
                            } catch (NumberFormatException e) {
                                throwAnimeParseException(parser, e.getMessage());
                            }
                        } else {
                            throwAnimeParseException(parser, "<" + tagName + "> tag must has the 'id' attributes");
                        }
                    } else if (!skipThisFrame && frame != null) {
                        // tag sprite
                        if (tagName.equalsIgnoreCase(DEF.TAG_SPRITE)) {
                            // the sprite
                            updateKeyValueMap(parser);
                            if (mKeyValueTempMap.containsKey(DEF.KEY_ID)) {
                                int id = 0;
                                float x = 0, y = 0, alpha = 1;
                                String value = null;
                                try {
                                    id = Integer.valueOf(mKeyValueTempMap.get(DEF.KEY_ID));
                                    // check the resource pool
                                    sprite = mResourceCache.getSprite(id);
                                    // if null mean need create this sprite
                                    if (sprite == null) {
                                        sprite = new Sprite();
                                        sprite.setId(id);
                                        mResourceCache.putSprite(sprite);
                                    }

                                    if ((value = mKeyValueTempMap.get(DEF.KEY_X)) != null) { // x
                                        x = getValue(parser, value);
                                    }
                                    if ((value = mKeyValueTempMap.get(DEF.KEY_Y)) != null) { // y
                                        y = getValue(parser, value);
                                    }
                                    if ((value = mKeyValueTempMap.get(DEF.KEY_ALPHA)) != null) { // alpha
                                        alpha = Float.valueOf(value);
                                    }

                                    // create a sprite draw state
                                    spriteDrawState = new SpriteDrawState(x, y, alpha);
                                } catch (NumberFormatException e) {
                                    throwAnimeParseException(parser, e.getMessage());
                                }
                            } else {
                                throwAnimeParseException(parser, "<" + tagName + "> tag must has the 'id' attributes");
                            }
                        } else if (tagName.equalsIgnoreCase(DEF.TAG_ROTATE) && spriteDrawState != null) { // rotate
                            updateKeyValueMap(parser);
                            // degree
                            if (mKeyValueTempMap.containsKey(DEF.KEY_DEGREE)) {
                                float degree = 0, x = 0, y = 0;
                                try {
                                    degree = Float.valueOf(mKeyValueTempMap.get(DEF.KEY_DEGREE));
                                    String value = null;
                                    if ((value = mKeyValueTempMap.get(DEF.KEY_X)) != null) { // x
                                        x = getValue(parser, value);
                                    }
                                    if ((value = mKeyValueTempMap.get(DEF.KEY_Y)) != null) { // y
                                        y = getValue(parser, value);
                                    }
                                    // set the rotate
                                    spriteDrawState.setRotate(x, y, degree);
                                } catch (NumberFormatException e) {
                                    throwAnimeParseException(parser, e.getMessage());
                                }
                            } else {
                                throwAnimeParseException(parser, "<" + tagName + "> tag must has the '" + DEF.KEY_DEGREE + "'attributes");
                            }
                        } else if (tagName.equalsIgnoreCase(DEF.TAG_SCALE) && spriteDrawState != null) {
                            updateKeyValueMap(parser);
                            // check scale x or scale y
                            if (mKeyValueTempMap.containsKey(DEF.KEY_SCALE_X) && mKeyValueTempMap.containsKey(DEF.KEY_SCALE_Y)) {
                                float sx = 0, sy = 0, x = 0, y = 0;
                                sx = Float.valueOf(mKeyValueTempMap.get(DEF.KEY_SCALE_X));
                                sy = Float.valueOf(mKeyValueTempMap.get(DEF.KEY_SCALE_Y));

                                String value = null;
                                if ((value = mKeyValueTempMap.get(DEF.KEY_X)) != null) { // x
                                    x = getValue(parser, value);
                                }
                                if ((value = mKeyValueTempMap.get(DEF.KEY_Y)) != null) { // y
                                    y = getValue(parser, value);
                                }

                                spriteDrawState.setScale(x, y, sx, sy);
                            } else {
                                throwAnimeParseException(parser, "<" + tagName + "> tag must has the '" + DEF.KEY_SCALE_X + "' and '" + DEF.KEY_SCALE_Y + "'attributes");
                            }
                        }
                    }

                    passRoot = true;
                } else if (eventType == XmlPullParser.END_TAG) {
                    if (tagName.equalsIgnoreCase(DEF.TAG_FRAME)) {
                        skipThisFrame = false;
                        frame = null;
                    } else if (tagName.equalsIgnoreCase(DEF.TAG_SPRITE)) {
                        if (frame != null && spriteDrawState != null && sprite != null) {
                            Log.i(TAG, spriteDrawState.toString());
                            frame.addSprite(sprite, spriteDrawState);
                        }
                        sprite = null;
                        spriteDrawState = null;
                    }
                }

                eventType = parser.next();
            }
        } catch (Exception e) {
            throw new AnimeParseException(e.getMessage());
        }
        if (parser != null) {
            parser.close();
        }
    }

    private void parseSprites(Anime anime, int spritesId) throws AnimeParseException {
        XmlResourceParser parser = null;
        try {
            parser = mResources.getXml(spritesId);
            int eventType = parser.getEventType();

            // start parse anime
            boolean passRoot = false;
            Log.i(TAG, "start parse sprite file");
            while (eventType != XmlPullParser.END_DOCUMENT) {
                String tagName = parser.getName();
                if (eventType == XmlPullParser.START_TAG) {
                    Log.i(TAG, "TAG:" + tagName);
                    // check the root
                    if (tagName.equalsIgnoreCase(DEF.ROOT_SPRITES_KEY)) {
                        if (passRoot) {
                            throwAnimeParseException(parser, "<" + tagName + "> tag must be the root tag");
                        }
                    } else if (!passRoot) {
                        throwAnimeParseException(parser, "<" + tagName + "> can not be the root tag");
                    } else if (tagName.equalsIgnoreCase(DEF.TAG_SPRITE)) {
                        updateKeyValueMap(parser);
                        if (mKeyValueTempMap.containsKey(DEF.KEY_ID) && mKeyValueTempMap.containsKey(DEF.KEY_BITMAP)) {
                            int id = -1;
                            try {
                                id = Integer.valueOf(mKeyValueTempMap.get(DEF.KEY_ID));
                            } catch (NumberFormatException e) {
                                throwAnimeParseException(parser, e.getMessage());
                            }

                            Sprite sprite = null;

                            // check the anime need this or not
                            int count = anime.getFrameCount();
                            for (int i = 0; i < count; i++) {
                                Frame frame = anime.getFrame(i);
                                int count2 = frame.getSpriteCount();
                                for (int j = 0; j < count2; j++) {
                                    if (frame.getSprite(j).getId() == id) {
                                        sprite = frame.getSprite(j);
                                        break;
                                    }
                                }
                            }

                            // use this sprite need init it
                            if (sprite != null) {
                                // check the pool contained
                                if (mResourceCache.getSprite(id) == null) {
                                    mResourceCache.putSprite(sprite);
                                }
                                // mean pool has it, but the bitmap dont load,
                                // otherwise skip
                                if (!sprite.isBitmapOK()) {
                                    String value = null;
                                    if ((value = mKeyValueTempMap.get(DEF.KEY_BITMAP)) != null) {
                                        sprite.setBitmap(getBitmap(parser, value));
                                    }
                                }
                            }
                        } else {
                            throwAnimeParseException(parser, "<" + tagName + "> tag must has the '" + DEF.KEY_ID + "' and '" + DEF.KEY_BITMAP + "'attributes");
                        }
                    }
                    passRoot = true;
                }

                eventType = parser.next();
            }
        } catch (Exception e) {
            throw new AnimeParseException(e.getMessage());
        }
        if (parser != null) {
            parser.close();
        }
    }

    float getValue(XmlPullParser parser, String value) throws AnimeParseException, NumberFormatException {
        float result = 0;
        if (value.startsWith(DEF.REF_HEAD_DIMEN)) {
            String name = value.substring(DEF.REF_HEAD_DIMEN.length() + 1);
            Log.i(TAG, "parse reference:" + name);
            int id = mResources.getIdentifier(name, "dimen", mPkgName);
            if (id == 0) {
                throwAnimeParseException(parser, value + " can't find this resource");
            } else {
                result = mResources.getDimension(id);
            }
        } else {
            result = Float.valueOf(value);
        }
        return result;
    }

    Bitmap getBitmap(XmlPullParser parser, String value) throws AnimeParseException {
        if (value.startsWith(DEF.REF_HEAD_DRAWABLE)) {
            String name = value.substring(DEF.REF_HEAD_DRAWABLE.length() + 1);
            int id = mResources.getIdentifier(name, "drawable", mPkgName);
            if (id == 0) {
                throwAnimeParseException(parser, value + " can't find this resource");
            } else {
                return BitmapFactory.decodeResource(mResources, id, options);
            }
        }
        return null;
    }

    void updateKeyValueMap(XmlResourceParser parser) {
        mKeyValueTempMap.clear();
        int size = parser.getAttributeCount();
        for (int i = 0; i < size; i++) {
            mKeyValueTempMap.put(parser.getAttributeName(i), parser.getAttributeValue(i));
        }
    }

    void throwAnimeParseException(XmlPullParser parser, String error) throws AnimeParseException {
        throw new AnimeParseException(String.format(ERROR_FORMAT_MESSAGE, parser.getLineNumber(), parser.getColumnNumber(), error));
    }

}
